/*
 *	This file is part of Transdroid <http://www.transdroid.org>
 *	
 *	Transdroid is free software: you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation, either version 3 of the License, or
 *	(at your option) any later version.
 *	
 *	Transdroid is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *	GNU General Public License for more details.
 *	
 *	You should have received a copy of the GNU General Public License
 *	along with Transdroid.  If not, see <http://www.gnu.org/licenses/>.
 *	
 */
package org.transdroid.daemon.Transmission;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.StringWriter;
import java.net.URI;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.base64.android.Base64;
import org.base64.android.Base64.InputStream;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.transdroid.daemon.Daemon;
import org.transdroid.daemon.DaemonException;
import org.transdroid.daemon.DaemonException.ExceptionType;
import org.transdroid.daemon.DaemonSettings;
import org.transdroid.daemon.IDaemonAdapter;
import org.transdroid.daemon.Priority;
import org.transdroid.daemon.Torrent;
import org.transdroid.daemon.TorrentDetails;
import org.transdroid.daemon.TorrentFile;
import org.transdroid.daemon.TorrentStatus;
import org.transdroid.daemon.task.AddByFileTask;
import org.transdroid.daemon.task.AddByMagnetUrlTask;
import org.transdroid.daemon.task.AddByUrlTask;
import org.transdroid.daemon.task.DaemonTask;
import org.transdroid.daemon.task.DaemonTaskFailureResult;
import org.transdroid.daemon.task.DaemonTaskResult;
import org.transdroid.daemon.task.DaemonTaskSuccessResult;
import org.transdroid.daemon.task.GetFileListTask;
import org.transdroid.daemon.task.GetFileListTaskSuccessResult;
import org.transdroid.daemon.task.GetStatsTask;
import org.transdroid.daemon.task.GetStatsTaskSuccessResult;
import org.transdroid.daemon.task.GetTorrentDetailsTask;
import org.transdroid.daemon.task.GetTorrentDetailsTaskSuccessResult;
import org.transdroid.daemon.task.PauseTask;
import org.transdroid.daemon.task.RemoveTask;
import org.transdroid.daemon.task.ResumeTask;
import org.transdroid.daemon.task.RetrieveTask;
import org.transdroid.daemon.task.RetrieveTaskSuccessResult;
import org.transdroid.daemon.task.SetAlternativeModeTask;
import org.transdroid.daemon.task.SetDownloadLocationTask;
import org.transdroid.daemon.task.SetFilePriorityTask;
import org.transdroid.daemon.task.SetTransferRatesTask;
import org.transdroid.daemon.util.DLog;
import org.transdroid.daemon.util.HttpHelper;

/**
 * The daemon adapter from the Transmission torrent client.
 * 
 * @author erickok
 *
 */
public class TransmissionAdapter implements IDaemonAdapter {

	private static final String LOG_NAME = "Transdroid daemon";
	
	private static final int FOR_ALL = -1;
	
	private static final String RPC_ID = "id";
	private static final String RPC_NAME = "name";
	private static final String RPC_STATUS = "status";
	private static final String RPC_ERROR = "error";
	private static final String RPC_ERRORSTRING = "errorString";
	private static final String RPC_DOWNLOADDIR = "downloadDir";
	private static final String RPC_RATEDOWNLOAD = "rateDownload";
	private static final String RPC_RATEUPLOAD = "rateUpload";
	private static final String RPC_PEERSGETTING = "peersGettingFromUs";
	private static final String RPC_PEERSSENDING = "peersSendingToUs";
	private static final String RPC_PEERSCONNECTED = "peersConnected";
	//private static final String RPC_PEERSKNOWN = "peersKnown";
	private static final String RPC_ETA = "eta";
	private static final String RPC_DOWNLOADSIZE1 = "haveUnchecked";
	private static final String RPC_DOWNLOADSIZE2 = "haveValid";
	private static final String RPC_UPLOADEDEVER = "uploadedEver";
	private static final String RPC_TOTALSIZE = "sizeWhenDone";
	private static final String RPC_DATEADDED = "addedDate";
	private static final String RPC_DATEDONE = "doneDate";
	private static final String RPC_AVAILABLE = "desiredAvailable";
	private static final String RPC_COMMENT = "comment";
	
	private static final String RPC_FILE_NAME = "name";
	private static final String RPC_FILE_LENGTH = "length";
	private static final String RPC_FILE_COMPLETED = "bytesCompleted";
	private static final String RPC_FILESTAT_WANTED = "wanted";
	private static final String RPC_FILESTAT_PRIORITY = "priority";
	
	private DaemonSettings settings;
	private DefaultHttpClient httpclient;
	private String sessionToken;

	private long rpcVersion = -1;
	
	public TransmissionAdapter(DaemonSettings settings) {
		this.settings = settings;
	}
	
	@Override
	public DaemonTaskResult executeTask(DaemonTask task) {
		
		try {
			
			// Get the server version
			if (rpcVersion <= -1) {
				// Get server session statistics
				JSONObject response = makeRequest(buildRequestObject("session-get", new JSONObject()));
				rpcVersion = response.getJSONObject("arguments").getInt("rpc-version");
			}
			
			JSONObject request = new JSONObject();
			switch (task.getMethod()) {
			case Retrieve:

				// Request all torrents from server
				JSONArray fields = new JSONArray();
				final String[] fieldsArray = new String[] { RPC_ID, RPC_NAME, RPC_ERROR, RPC_ERRORSTRING, RPC_STATUS, 
						RPC_DOWNLOADDIR, RPC_RATEDOWNLOAD, RPC_RATEUPLOAD, RPC_PEERSGETTING, RPC_PEERSSENDING, 
						RPC_PEERSCONNECTED, RPC_ETA, RPC_DOWNLOADSIZE1, RPC_DOWNLOADSIZE2, RPC_UPLOADEDEVER, 
						RPC_TOTALSIZE, RPC_DATEADDED, RPC_DATEDONE, RPC_AVAILABLE, RPC_COMMENT }; 
				for (String field : fieldsArray) {
					fields.put(field);
				}
				request.put("fields", fields);

				JSONObject result = makeRequest(buildRequestObject("torrent-get", request));
				return new RetrieveTaskSuccessResult((RetrieveTask) task, parseJsonRetrieveTorrents(result.getJSONObject("arguments")),null);

			case GetStats:

				// Request the current server statistics
				JSONObject stats = makeRequest(buildRequestObject("session-get", new JSONObject())).getJSONObject("arguments");
				return new GetStatsTaskSuccessResult((GetStatsTask) task, stats.getBoolean("alt-speed-enabled"), 
						rpcVersion >= 12? stats.getLong("download-dir-free-space"): -1);

			case GetTorrentDetails:

				// Request fine details of a specific torrent 
				JSONArray dfields = new JSONArray();
				dfields.put("trackers");
				dfields.put("trackerStats");

				JSONObject buildDGet = buildTorrentRequestObject(task.getTargetTorrent().getUniqueID(), null, false);
				buildDGet.put("fields", dfields);
				JSONObject getDResult = makeRequest(buildRequestObject("torrent-get", buildDGet));
				return new GetTorrentDetailsTaskSuccessResult((GetTorrentDetailsTask) task, parseJsonTorrentDetails(getDResult.getJSONObject("arguments")));
				
			case GetFileList:

				// Request all details for a specific torrent 
				JSONArray ffields = new JSONArray();
				ffields.put("files");
				ffields.put("fileStats");
				
				JSONObject buildGet = buildTorrentRequestObject(task.getTargetTorrent().getUniqueID(), null, false);
				buildGet.put("fields", ffields);
				JSONObject getResult = makeRequest(buildRequestObject("torrent-get", buildGet));
				return new GetFileListTaskSuccessResult((GetFileListTask) task, parseJsonFileList(getResult.getJSONObject("arguments"), task.getTargetTorrent()));
				
			case AddByFile:
				
				// Add a torrent to the server by sending the contents of a local .torrent file
				String file = ((AddByFileTask)task).getFile();
				
				// Encode the .torrent file's data
				InputStream in = new Base64.InputStream(new FileInputStream(new File(URI.create(file))), Base64.ENCODE);
				StringWriter writer = new StringWriter();
				int c;
				while ((c = in.read())!= -1) {
					writer.write(c);
				}
				in.close();
				
				// Request to add a torrent by Base64-encoded meta data
				request.put("metainfo", writer.toString());

				makeRequest(buildRequestObject("torrent-add", request));
				return new DaemonTaskSuccessResult(task);

			case AddByUrl:

				// Request to add a torrent by URL
				String url = ((AddByUrlTask)task).getUrl();
				request.put("filename", url);
				
				makeRequest(buildRequestObject("torrent-add", request));
				return new DaemonTaskSuccessResult(task);

			case AddByMagnetUrl:

				// Request to add a magnet link by URL
				String magnet = ((AddByMagnetUrlTask)task).getUrl();
				request.put("filename", magnet);
				
				makeRequest(buildRequestObject("torrent-add", request));
				return new DaemonTaskSuccessResult(task);

			case Remove:

				// Remove a torrent
				RemoveTask removeTask = (RemoveTask) task;
				makeRequest(buildRequestObject("torrent-remove", buildTorrentRequestObject(removeTask.getTargetTorrent().getUniqueID(), "delete-local-data", removeTask.includingData())));
				return new DaemonTaskSuccessResult(task);
				
			case Pause:

				// Pause a torrent
				PauseTask pauseTask = (PauseTask) task;
				makeRequest(buildRequestObject("torrent-stop", buildTorrentRequestObject(pauseTask.getTargetTorrent().getUniqueID(), null, false)));
				return new DaemonTaskSuccessResult(task);
				
			case PauseAll:

				// Resume all torrents
				makeRequest(buildRequestObject("torrent-stop", buildTorrentRequestObject(FOR_ALL, null, false)));
				return new DaemonTaskSuccessResult(task);

			case Resume:

				// Resume a torrent
				ResumeTask resumeTask = (ResumeTask) task;
				makeRequest(buildRequestObject("torrent-start", buildTorrentRequestObject(resumeTask.getTargetTorrent().getUniqueID(), null, false)));
				return new DaemonTaskSuccessResult(task);
				
			case ResumeAll:

				// Resume all torrents
				makeRequest(buildRequestObject("torrent-start", buildTorrentRequestObject(FOR_ALL, null, false)));
				return new DaemonTaskSuccessResult(task);

			case SetDownloadLocation:
				
				// Change the download location
				SetDownloadLocationTask sdlTask = (SetDownloadLocationTask) task;
				// Build request
				JSONObject sdlrequest = new JSONObject();
				JSONArray sdlids = new JSONArray();
				sdlids.put(Long.parseLong(task.getTargetTorrent().getUniqueID()));
				sdlrequest.put("ids", sdlids);
				sdlrequest.put("location", sdlTask.getNewLocation());
				sdlrequest.put("move", true);
				makeRequest(buildRequestObject("torrent-set-location", sdlrequest));
				return new DaemonTaskSuccessResult(task);
				
			case SetFilePriorities:

				// Set priorities of the files of some torrent
				SetFilePriorityTask prioTask = (SetFilePriorityTask) task;

				// Build request
				JSONObject prequest = new JSONObject();
				JSONArray ids = new JSONArray();
				ids.put(Long.parseLong(task.getTargetTorrent().getUniqueID()));
				prequest.put("ids", ids);
				JSONArray fileids = new JSONArray();
				for (TorrentFile forfile : prioTask.getForFiles()) {
					fileids.put(Integer.parseInt(forfile.getKey())); // The keys are the indices of the files, so always numeric
				}
				switch (prioTask.getNewPriority()) {
				case Off:
					prequest.put("files-unwanted", fileids);
					break;
				case Low:
					prequest.put("files-wanted", fileids);
					prequest.put("priority-low", fileids);
					break;
				case Normal:
					prequest.put("files-wanted", fileids);
					prequest.put("priority-normal", fileids);
					break;
				case High:
					prequest.put("files-wanted", fileids);
					prequest.put("priority-high", fileids);
					break;
				}
				
				makeRequest(buildRequestObject("torrent-set", prequest));
				return new DaemonTaskSuccessResult(task);

			case SetTransferRates:
				
				// Request to set the maximum transfer rates
				SetTransferRatesTask ratesTask = (SetTransferRatesTask) task;
				if (ratesTask.getUploadRate() == null) {
					request.put("speed-limit-up-enabled", false);
				} else {
					request.put("speed-limit-up-enabled", true);
					request.put("speed-limit-up", ratesTask.getUploadRate().intValue());
				}
				if (ratesTask.getDownloadRate() == null) {
					request.put("speed-limit-down-enabled", false);
				} else {
					request.put("speed-limit-down-enabled", true);
					request.put("speed-limit-down", ratesTask.getDownloadRate().intValue());
				}

				makeRequest(buildRequestObject("session-set", request));
				return new DaemonTaskSuccessResult(task);

			case SetAlternativeMode:
				
				// Request to set the alternative speed mode (Tutle Mode)
				SetAlternativeModeTask altModeTask = (SetAlternativeModeTask) task;
				request.put("alt-speed-enabled", altModeTask.isAlternativeModeEnabled());
				makeRequest(buildRequestObject("session-set", request));
				return new DaemonTaskSuccessResult(task);
				
			default:
				return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.MethodUnsupported, task.getMethod() + " is not supported by " + getType()));
			}
		} catch (JSONException e) {
			return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.ParsingFailed, e.toString()));
		} catch (DaemonException e) {
			return new DaemonTaskFailureResult(task, e);
		} catch (FileNotFoundException e) {
			return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
		} catch (IOException e) {
			return new DaemonTaskFailureResult(task, new DaemonException(ExceptionType.FileAccessError, e.toString()));
		}
	}
	
	private JSONObject buildTorrentRequestObject(String torrentID, String extraKey, boolean extraValue ) throws JSONException {
		return buildTorrentRequestObject(Long.parseLong(torrentID), extraKey, extraValue);
	}
	
	private JSONObject buildTorrentRequestObject(long torrentID, String extraKey, boolean extraValue ) throws JSONException {

		// Build request for one specific torrent
		JSONObject request = new JSONObject();
		if (torrentID != FOR_ALL) {
			JSONArray ids = new JSONArray();
			ids.put(torrentID); // The only id to add
			request.put("ids", ids);
		}
		if (extraKey != null) {
			request.put(extraKey, extraValue);
		}
		return request;

	}
	
	private JSONObject buildRequestObject(String sendMethod, JSONObject arguments) throws JSONException {

		// Build request for method
		JSONObject request = new JSONObject();
		request.put("method", sendMethod);
		request.put("arguments", arguments);
		request.put("tag", 0);
		return request;
	}
	
	private JSONObject makeRequest(JSONObject data) throws DaemonException {

		try {
				
			// Initialise the HTTP client
			if (httpclient == null) {
				initialise();
			}
			final String sessionHeader = "X-Transmission-Session-Id";
	
			// Setup request using POST stream with URL and data
			HttpPost httppost = new HttpPost(buildWebUIUrl());
			StringEntity se = new StringEntity(data.toString(), "UTF-8");
			httppost.setEntity(se);
	
			// Send the stored session token as a header
			if (sessionToken != null) {
				httppost.addHeader(sessionHeader, sessionToken);
			}
			
			// Execute
			HttpResponse response = httpclient.execute(httppost);
			
			// Authentication error?
			if (response.getStatusLine().getStatusCode() == 401) { 
				throw new DaemonException(ExceptionType.AuthenticationFailure, "401 HTTP response (username or password incorrect)");
			}
			
			// 409 error because of a session id?
			if (response.getStatusLine().getStatusCode() == 409) {
				
				// Retry post, but this time with the new session token that was encapsulated in the 409 response
				sessionToken = response.getFirstHeader(sessionHeader).getValue();
				httppost.addHeader(sessionHeader, sessionToken);
				response = httpclient.execute(httppost);
				
			}
			
			HttpEntity entity = response.getEntity();
			if (entity != null) {
				
				// Read JSON response
				java.io.InputStream instream = entity.getContent();
				String result = HttpHelper.ConvertStreamToString(instream);
				JSONObject json = new JSONObject(result);
				instream.close();
				
				//TLog.d(LOG_NAME, "Success: " + (result.length() > 200? result.substring(0, 200) + "... (" + result.length() + " chars)": result));
				
				// Return the JSON object
				return json;
			}

			DLog.d(LOG_NAME, "Error: No entity in HTTP response");
			throw new DaemonException(ExceptionType.UnexpectedResponse, "No HTTP entity object in response.");

		} catch (DaemonException e) {
			throw e;
		} catch (JSONException e) {
			DLog.d(LOG_NAME, "Error: " + e.toString());
			throw new DaemonException(ExceptionType.ParsingFailed, e.toString());
		} catch (Exception e) {
			DLog.d(LOG_NAME, "Error: " + e.toString());
			throw new DaemonException(ExceptionType.ConnectionError, e.toString());
		}
		
	}

	/**
	 * Instantiates an HTTP client with proper credentials that can be used for all Transmission requests.
	 * @param connectionTimeout The connection timeout in milliseconds
	 * @throws DaemonException On conflicting or missing settings
	 */
	private void initialise() throws DaemonException {
		httpclient = HttpHelper.createStandardHttpClient(settings, true);
	}
	
	/**
	 * Build the URL of the Transmission web UI from the user settings.
	 * @return The URL of the RPC API
	 */
	private String buildWebUIUrl() {
		return (settings.getSsl() ? "https://" : "http://") + settings.getAddress() + ":" + settings.getPort() + (settings.getFolder() == null? "": settings.getFolder()) + "/transmission/rpc";
	}

	private ArrayList<Torrent> parseJsonRetrieveTorrents(JSONObject response) throws JSONException {
	
		// Parse response
		ArrayList<Torrent> torrents = new ArrayList<Torrent>();
		JSONArray rarray = response.getJSONArray("torrents");
		for (int i = 0; i < rarray.length(); i++) {
			JSONObject tor = rarray.getJSONObject(i);
			// Add the parsed torrent to the list
			float have = (float)(tor.getLong(RPC_DOWNLOADSIZE1) + tor.getLong(RPC_DOWNLOADSIZE2));
			long total = tor.getLong(RPC_TOTALSIZE);
			// Error is a number, see https://trac.transmissionbt.com/browser/trunk/libtransmission/transmission.h#L1747
			// We only consider it a real error if it is local (blocking), which is error code 3
			boolean hasError = tor.getInt(RPC_ERROR) == 3;
			String errorString = tor.getString(RPC_ERRORSTRING).trim();
			String commentString = tor.getString(RPC_COMMENT).trim();
			if (!commentString.equals("")) {
				errorString = errorString.equals("")? commentString : errorString + "\n" + commentString;
			}
			torrents.add(new Torrent(
					tor.getInt(RPC_ID),
					null,
					tor.getString(RPC_NAME),
					hasError? TorrentStatus.Error: getStatus(tor.getInt(RPC_STATUS)),
					tor.getString(RPC_DOWNLOADDIR) + settings.getOS().getPathSeperator(),
					tor.getInt(RPC_RATEDOWNLOAD),
					tor.getInt(RPC_RATEUPLOAD),
					tor.getInt(RPC_PEERSGETTING),
					tor.getInt(RPC_PEERSSENDING),
					tor.getInt(RPC_PEERSCONNECTED),
					tor.getInt(RPC_PEERSCONNECTED),
					tor.getInt(RPC_ETA),
					tor.getLong(RPC_DOWNLOADSIZE1) + tor.getLong(RPC_DOWNLOADSIZE2),
					tor.getLong(RPC_UPLOADEDEVER),
					tor.getLong(RPC_TOTALSIZE),
					//(float) tor.getDouble(RPC_PERCENTDONE),
					(total == 0? 0: have/(float)total),
					(total == 0? 0: (have+(float)tor.getLong(RPC_AVAILABLE))/(float)total),
					null, // No label/category/group support in the RPC API for now
					new Date(tor.getLong(RPC_DATEADDED) * 1000L),
					new Date(tor.getLong(RPC_DATEDONE) * 1000L),
					errorString));
		}
		
		// Return the list
		return torrents;
		
	}

	private TorrentStatus getStatus(int status) {
		if (rpcVersion <= -1) {
			return TorrentStatus.Unknown;
		} else if (rpcVersion >= 14) {
			switch (status) {
			case 0:
				return TorrentStatus.Paused;
			case 1:
				return TorrentStatus.Waiting;
			case 2:
				return TorrentStatus.Checking;
			case 3:
				return TorrentStatus.Queued;
			case 4:
				return TorrentStatus.Downloading;
			case 5:
				return TorrentStatus.Queued;
			case 6:
				return TorrentStatus.Seeding;
			}
			return TorrentStatus.Unknown;
		} else {
			return TorrentStatus.getStatus(status);
		}
	}

	private ArrayList<TorrentFile> parseJsonFileList(JSONObject response, Torrent torrent) throws JSONException {
	
		// Parse response
		ArrayList<TorrentFile> torrentfiles = new ArrayList<TorrentFile>();
		JSONArray rarray = response.getJSONArray("torrents");
		if (rarray.length() > 0) {
			JSONArray files = rarray.getJSONObject(0).getJSONArray("files");
			JSONArray fileStats = rarray.getJSONObject(0).getJSONArray("fileStats");
			for (int i = 0; i < files.length(); i++) {
				JSONObject file = files.getJSONObject(i);
				JSONObject stat = fileStats.getJSONObject(i);
				torrentfiles.add(new TorrentFile(
						"" + i,
						file.getString(RPC_FILE_NAME),
						file.getString(RPC_FILE_NAME),
						torrent.getLocationDir() + file.getString(RPC_FILE_NAME),
						file.getLong(RPC_FILE_LENGTH),
						file.getLong(RPC_FILE_COMPLETED),
						convertTransmissionPriority(stat.getBoolean(RPC_FILESTAT_WANTED), stat.getInt(RPC_FILESTAT_PRIORITY))));
			}
		}
		
		// Return the list
		return torrentfiles;
		
	}

	private Priority convertTransmissionPriority(boolean isWanted, int priority) {
		if (!isWanted) {
			return Priority.Off;
		} else {
			switch (priority) {
			case 1:
				return Priority.High;
			case -1:
				return Priority.Low;
			default:
				return Priority.Normal;
			}
		}
	}

	private TorrentDetails parseJsonTorrentDetails(JSONObject response) throws JSONException {
	
		// Parse response
		// NOTE: Assumes only details for one torrent are requested at a time
		JSONArray rarray = response.getJSONArray("torrents");
		if (rarray.length() > 0) {
			JSONArray trackersList = rarray.getJSONObject(0).getJSONArray("trackers");
			List<String> trackers = new ArrayList<String>();
			for (int i = 0; i < trackersList.length(); i++) {
				trackers.add(trackersList.getJSONObject(i).getString("announce"));
			}
			JSONArray trackerStatsList = rarray.getJSONObject(0).getJSONArray("trackerStats");
			List<String> errors = new ArrayList<String>();
			for (int i = 0; i < trackerStatsList.length(); i++) {
				// Get the tracker response and if it was an error then add it
				String lar = trackerStatsList.getJSONObject(i).getString("lastAnnounceResult");
				if (lar != null && !lar.equals("") && !lar.equals("Success")) {
					errors.add(lar);
				}
			}
			return new TorrentDetails(trackers, errors);
		}
		
		return null;
		
	}

	@Override
	public Daemon getType() {
		return settings.getType();
	}

	@Override
	public DaemonSettings getSettings() {
		return this.settings;
	}
	
}
